package be.kuleuven.cs.mop.domain.model.impl;


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import be.kuleuven.cs.mop.domain.exceptions.BusinessRuleThreeViolationExceptionDeadlineExpired;
import be.kuleuven.cs.mop.domain.exceptions.BusinessRuleThreeViolationExceptionTaskScheduleDoesNotCoverCurrentTime;
import be.kuleuven.cs.mop.domain.exceptions.BusinessRuleTwoViolationExceptionTask;
import be.kuleuven.cs.mop.domain.exceptions.BusinessRuleTwoViolationExceptionDependantTasks;
import be.kuleuven.cs.mop.domain.exceptions.TaskManagerException;
import be.kuleuven.cs.mop.domain.model.TaskStatus;


/**
 * Abstract class representing a state
 * State transitions are done by the states, therefore setStateXXX default methods are present here and overridden in subclasses when necessary
 */
public abstract class TaskState {
	
	/*
	 * The task this state belongs to
	 */
	private final TaskImpl task;
	
	
	protected TaskState(final TaskImpl task) {
		if (task == null)
			throw new NullPointerException("Task == NULL");
		
		this.task = task;
	}
	
	/**
	 * Returns the task this state belongs to
	 */
	protected TaskImpl getTask() { return task; }
	
	/**
	 * Default case for when the system time changes
	 * Enforces appropriate business rules
	 * @param time - The new system time
	 */
	public void onTimeChanged(final Clock time) {
		enforceBusinessRule3(time);
	}
	
	/**
	 * Default case for enforcing (the second part of) business rule 3
	 * @param time - The new time of the system
	 * 
	 * @Note: 	the first part of business rule 3 is never checked, because the system specifications say time can only be advanced,
	 * 			and system starts in a consistent state
	 */
	protected void enforceBusinessRule3(Clock time) {
		if (getTask().deadlineExpired())
			try {
				getTask().updateTaskStatusRecursively(TaskStatus.FAILED);
			} catch (TaskManagerException e) {
				// This should never happen
				throw new InternalError();
			}
	}
	
	/**
	 * Changes this state to failed
	 * Checks appropriate transition (business) rules first
	 * @throws BusinessRuleTwoViolationExceptionDependantTasks - One of the dependant tasks' status is not failed 
	 */
	public void setFailedSafely() throws BusinessRuleTwoViolationExceptionDependantTasks {
		checkBusinessRule2ToFailedRecursiveCase();
		setFailed();
	}
	
	/**
	 * Changes this state to successful
	 * Checks appropriate transition (business) rules first
	 * 
	 * @throws BusinessRuleTwoViolationExceptionTask - One of the dependencies of the task this state belongs to is not completed successfully yet
	 * @throws TaskManagerException TASK_DOES_NOT_MEET_HELPER_USER_REQUIREMENTS - The minimum of helper users for this task is not met
	 * @throws TaskManagerException TASK_DOES_NOT_MEET_RESOURCE_RESERVATION_REQUIREMENTS - The resource reservation requirements for this task are not met
	 * @throws BusinessRuleThreeViolationExceptionTaskScheduleDoesNotCoverCurrentTime - {@link BusinessRuleThreeViolationExceptionTaskScheduleDoesNotCoverCurrentTime#DEFAULT_MESSAGE}
	 */
	public void setSuccessfulSafely() throws BusinessRuleTwoViolationExceptionTask, TaskManagerException, BusinessRuleThreeViolationExceptionTaskScheduleDoesNotCoverCurrentTime {
		for (final TaskImpl dependency : getTask().getDependencies())
			if (!dependency.getState().isSuccessful())
				throw new BusinessRuleTwoViolationExceptionTask(dependency);
		if(!getTask().satisfiesAcceptedHelperUserRequirements()){
				throw new TaskManagerException(TaskManagerException.TASK_DOES_NOT_MEET_HELPER_USER_REQUIREMENTS);
			}
		if(!getTask().satisfiesReservationRequirements()){
				throw new TaskManagerException(TaskManagerException.TASK_DOES_NOT_MEET_RESOURCE_RESERVATION_REQUIREMENTS);
			}
		if(!getTask().scheduleCoversCurrentTime())
			throw new BusinessRuleThreeViolationExceptionTaskScheduleDoesNotCoverCurrentTime();
		
		setSuccessful();
	}
	
	/**
	 * Changes this state to unfinished 
	 * Checks appropriate transition (business) rules first
	 * 
	 * @throws BusinessRuleTwoViolationExceptionTask - {@link TaskStateFailed#setUnfinishedSafely()}
	 * @throws BusinessRuleTwoViolationExceptionDependantTasks - One of the dependant tasks does not have status failed/unavailable
	 * @throws BusinessRuleThreeViolationExceptionDeadlineExpired - {@link BusinessRuleThreeViolationExceptionDeadlineExpired#DEFAULT_MESSAGE}
	 */
	public void setUnfinishedSafely() throws BusinessRuleTwoViolationExceptionTask, BusinessRuleTwoViolationExceptionDependantTasks, BusinessRuleThreeViolationExceptionDeadlineExpired {
		checkBusinessRule3DeadlineExpired();
		checkBusinessRule2ToUnfinishedRecursiveCase();
		setUnfinished();
	}
	
	/**
	 * Checks business rule 3 (second part) when changing this state to unfinished
	 * @throws BusinessRuleThreeViolationExceptionDeadlineExpired - {@link BusinessRuleThreeViolationExceptionDeadlineExpired#DEFAULT_MESSAGE}
	 */
	private void checkBusinessRule3DeadlineExpired() throws BusinessRuleThreeViolationExceptionDeadlineExpired {
		if(getTask().deadlineExpired())
			throw new BusinessRuleThreeViolationExceptionDeadlineExpired();
	}
	
	/**
	 * General Business Rule 2 check when state is changed to failed (first part business rule 2)
	 * All tasks that depend on the task this state belongs to need to be failed
	 * 
	 * @throws BusinessRuleTwoViolationExceptionDependantTasks - At least one of the dependant tasks' status is not failed
	 */
	private void checkBusinessRule2ToFailedRecursiveCase() throws BusinessRuleTwoViolationExceptionDependantTasks {
		final HashMap<TaskImpl, TaskStatus> suggestions = new HashMap<TaskImpl, TaskStatus>();
		final List<TaskImpl>	tasks;
		List<TaskStatus> allowedStates = new ArrayList<TaskStatus>();
		allowedStates.add(TaskStatus.FAILED);
		tasks = findAllViolatingDependantsRecursively(getTask(), allowedStates);
		for(TaskImpl task: tasks){
			suggestions.put(task, TaskStatus.FAILED);
		}
		if (!tasks.isEmpty())
			throw new BusinessRuleTwoViolationExceptionDependantTasks(suggestions);
	}
	
	/**
	 * General Business Rule 2 check when state is changed to unfinished (second part business rule 2)
	 * All tasks that depend on the task this state belongs to need to be failed/unavailable
	 * 
	 * @throws BusinessRuleTwoViolationExceptionDependantTasks - At least one of the dependant tasks' status is not failed/unavailable
	 */
	private void checkBusinessRule2ToUnfinishedRecursiveCase() throws BusinessRuleTwoViolationExceptionDependantTasks {
		final HashMap<TaskImpl, TaskStatus> suggestions = new HashMap<TaskImpl, TaskStatus>();
		final List<TaskImpl>	tasks;
		List<TaskStatus> allowedStates = new ArrayList<TaskStatus>();
		allowedStates.add(TaskStatus.FAILED);
		allowedStates.add(TaskStatus.UNFINISHED);
		tasks = findAllViolatingDependantsRecursively(getTask(), allowedStates);
		
		for(TaskImpl task : tasks){
			suggestions.put(task, TaskStatus.UNFINISHED);
		}
		if (!tasks.isEmpty())
			throw new BusinessRuleTwoViolationExceptionDependantTasks(suggestions );
	}
	
	/**
	 * Changes the state of this state's task to the given new state
	 * @param state - The new state
	 */
	protected void set(final TaskState state) {
		getTask().setStatus(state);
	}
	
	/**
	 * Changes this state to successful without checking any business rules
	 */
	protected void setSuccessful() {
		set(new TaskStateSuccessful(getTask()));
	}
	
	/**
	 * Changes this state to failed without checking any business rules
	 */
	protected void setFailed() {
		set(new TaskStateFailed(getTask()));
	}
	
	/**
	 * Changes the state to TaskStateUnfinished, without checking any business rules
	 */
	protected void setUnfinished() {
		set(new TaskStateUnfinished(getTask()));
	}
	
	/**
	 * Method to recursively find all dependant tasks from given task that don't have a state that is present in the allowedStates list
	 * @param task - The task to check the dependant tasks from
	 * @param allowedStates - The list of allowed states the dependants of the given task can have
	 * @return A list of dependant tasks that do not have an allowed state at this moment
	 */
	private List<TaskImpl> findAllViolatingDependantsRecursively (TaskImpl task, List<TaskStatus> allowedStates) {
		List<TaskImpl> result = new ArrayList<TaskImpl>();
		for (final TaskImpl dependant : task.getDependants()) {
			if (!allowedStates.contains(dependant.getStatus())) {
				result.add(dependant);
				result.addAll(findAllViolatingDependantsRecursively(dependant, allowedStates));
			}
		}
		return result;
	}
	
	/**
	 * Default case for state that is not modifiable
	 * Overridden in TaskStateUnfinished
	 */
	public boolean isModifiable() { return false; }

	/**
	 * Default case for state that is not available
	 * Overridden in TaskStateUnfinished
	 */
	public boolean isAvailable() { return false; }
	
	/**
	 * Default case for state that is not failed
	 * Overridden in TaskStateFailed
	 */
	public boolean isFailed() { return false; }
	
	/**
	 * Default case for state that is not successful
	 * Overridden in TaskStateSuccessful
	 */
	public boolean isSuccessful() { return false; }
	
	/**
	 * Default case for state that is not unfinished
	 * Overridden in TaskStateUnfinished
	 */
	public boolean isUnfinished() { return false; }
	
	/**
	 * Returns a <tt>TaskStatus</tt> which represents this <tt>TaskState</tt>
	 * @return a {@link TaskStatus}
	 */
	public abstract TaskStatus toStatus();
	
}